Skip to content

Conversation

@0x-r4bbit
Copy link
Contributor

Prior to this commits deployment lifecycle hooks had been defined as Array due
to historical reasons (contracts.js) used to be a json file back in the days.

deployIf, onDeploy and afterDeploy can now be defined as (async)
function and have access to several dependencies such as contract instances and web3.
However, in order to have needed dependencies registered in the dependencies object,
all lifecycle hook dependencies need to be listed in the deps property
as shown below.

Also note that this is NOT a breaking change. Existing deployment lifecycle
hooks written in Array style still work.

All three lifecycle hooks can now be defined as (async) functions and get an dependency
object with a shape like this:

interface DeploymentLifecycleHookDependencies {
  contracts: Map<string, ContractInstance>;
  web3: Web3Instance
}

deployIf lifecycle hook has to return a promise (or be defined using async/await and return
a value) like this:

contracts: {
  MyToken: {...},
  SimpleStorage: {
    deps: ['MyToken'], // this is needed to make `MyToken` available within `dependencies`
    deployIf: async (dependencies) => {
      return dependencies.contracts.MyToken_address;
    }
  },
}

Vanilla promises (instead of async/await) can be used as well:

contracts: {
  MyToken: {...},
  SimpleStorage: {
    deps: ['MyToken'],
    deployIf: (dependencies) => {
      return new Promise(resolve => resolve(dependencies.contracts.MyToken_address);
    }
  },
}

onDeploy as well, returns either a promise or is used using async/await:

contracts: {
  SimpleStorage: {
    onDeploy: async (dependencies) => {
      const simpleStorage = dependencies.contracts.SimpleStorage;
      const value = await simpleStorage.methods.get().call();
      console.log(value);
    }
  },
}

afterDeploy has automatically access to all configured and deployed contracts of the dapp:

contracts: {
  SimpleStorage: {...},
  MyToken: {...},
  afterDeploy: (dependencies) => {
    console.log('Done!');
  }
}

Closes #1029

@0x-r4bbit
Copy link
Contributor Author

Ah, this needs a rebase.

if (typeof self.config.contractsConfig.afterDeploy === 'function') {
this.getAfterDeployLifecycleHookDependencies().then(dependencies => {
(async () => {
await self.config.contractsConfig.afterDeploy(dependencies);
Copy link
Contributor Author

@0x-r4bbit 0x-r4bbit Nov 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice that we always call lifecycle hook within async/await context.

This also means that lifecycle hooks have to either (optionally) return a Promise, or being written in async/await themselves, to notify Embark that a hook is done

@alaibe I looked into supporting both, promise support as well as callback support, unfortunately, I couldn't make both APIs work, without the user worrying about how their lifecycle hooks are being called.

I also believe, users shouldn't be needed to do both at the same time (calling callback and returning promise).

So I decided to have them use either Promise APIs or async/await. This seems natural to me as well (especially because this is a new API and I don't think there's a good reason to keep callback based APIs around)

if (typeof cmd === 'function') {
this.getOnDeployLifecycleHookDependencies(contract).then(dependencies => {
const callbackFn = (value) => {
};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a left over.

return cb(new Error("error running afterDeploy"));
}
if (typeof self.config.contractsConfig.afterDeploy === 'function') {
this.getAfterDeployLifecycleHookDependencies().then(dependencies => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be worth putting the whole function in async/await as well, so that getAfterDeployLIfecyclehookDependencies() can be called with await, meaning also that afterDeploy() can be called with await without wrapping it in an IIFE.

});
}

getOnDeployLifecycleHookDependencies(contractConfig) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be rewritten using async/await

Copy link
Contributor Author

@0x-r4bbit 0x-r4bbit Nov 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I've realised those aren't easily converted to async/await because our entire event bus is callback-based.

});
}

getAfterDeployLifecycleHookDependencies() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be rewritten using async/await

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. Turns out we can't really convert this to async/await as there's not promise based API used within this function.


self.contractDependencies[className] = self.contractDependencies[className] || [];

if (contract.deps && contract.deps.length) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Array.isArray(contract.deps) same technique as before, seems more elegant?

return cb(new Error(`Error when registering onDeploy hook for ${contract.name}: ${err.message}`));
});
} else {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cosmetic: no extra line here?

if (typeof cmd === 'function') {
this.getOnDeployLifecycleHookDependencies(contract).then(dependencies => {
const callbackFn = (value) => {
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cosmetic: can be one line

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is actually a left over that has been removed in the meantime :)

});
}

getAfterDeployLifecycleHookDependencies() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getOnDeployLifecycleHookDependencies and getAfterDeployLifecycleHookDependencies seems to share some logic, do you think it could be possible to extract the sharing logic into function?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I was considering that too. Especially the part that puts together the dependencies ( think the rest is already pretty broken down into pieces and now really reusable).

But then I thought, it might be a little overkill to introduce a new function for:

          this.events.request('blockchain:get', web3 => {

            let dependencies = { contracts: {}, web3 };

            contractInstances.forEach(contractInstance => {
              dependencies.contracts[contractInstance.className] = contractInstance.instance;
            });
            resolve(dependencies);
          });

Anyways, will update it :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To reduce the shared code and also not having to re-create the contract object every time there is a new onDeploy, I think we should have one of the contract modules have a list of the web3 contracts an retrieve them here with an event.

Every time a contract is deployed, it would create that contract object. That would be quite useful for a couple of our modules. I'm thinking about this one, the ENS one, maybe even tests.

Maybe one of our modules already does that, but I don't recall. Also, maybe there is a good reason we didn't do that before (maybe a memory issue)? What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a very good point you're raising here @jrainville

From the top of my head I'd say ContractsManager should be responsible for that. re: Memory issues: AFAIK node has a max memory limit of ~1.5 GB by default but can be easily extended if needed.

I guess that's something we'll have to experiment with.

I believe this would be a good candidate for a separate PR/effort though.

@0x-r4bbit 0x-r4bbit force-pushed the feat/deploy-hook-functions branch 2 times, most recently from 204b001 to c9e445e Compare November 19, 2018 10:40
dependencies.contracts[contractInstance.className] = contractInstance.instance;
});
return dependencies;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alaibe this is the handler that's shared in both get*Dependencies() functions.

Notice that that's the only part that could potentially be shared.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough, can I suggest using reduce? That will be more declarative

@0x-r4bbit 0x-r4bbit force-pushed the feat/deploy-hook-functions branch 2 times, most recently from 7abf485 to 7452df5 Compare November 19, 2018 11:37
@0x-r4bbit
Copy link
Contributor Author

Documentation for this feature can be found here: embark-framework/embark-site#106

}

if (onDeploy && onDeploy.length) {
if (Array.isArray(onDeploy)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it have issues with empty arrays?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was actually the reason why I didn't use isArray in the first place, however, if it happens to be an empty array, it's fine as the following code path will basically be a noop.

if (Array.isArray(contract.deps)) {
contract.deps.forEach(name => {
self.contractDependencies[className].push(name);
});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can just do a concat self.contractDependencies[className] = self.contractDependencies[className].concat(contract.deps);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point!


getOnDeployLifecycleHookDependencies(contractConfig) {
let dependencyNames = contractConfig.deps || [];
dependencyNames.push(contractConfig.className);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the same contract can be added multiple times to the list like this. I don't think it's a super big deal, since it's only references, but we never know.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, we can dedupe dependencyNames before we enter the async.map()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deduped

async.map(dependencyNames, (contractName, next) => {
this.events.request('contracts:contract', contractName, (contractRecipe) => {
if (!contractRecipe) {
next(new Error('ReferredContractDoesNotExist'));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you put the name of the referred contract too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

} else if (!result) {
self.logger.info(params.contract.className + ' deployIf directive returned false; contract will not deploy');
params.shouldDeploy = false;
reject(err);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you don't have to return here, but I feel like it's safer to anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, no return statement needed for resolve/reject as this will return the promise immediately.

});
}

getAfterDeployLifecycleHookDependencies() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To reduce the shared code and also not having to re-create the contract object every time there is a new onDeploy, I think we should have one of the contract modules have a list of the web3 contracts an retrieve them here with an event.

Every time a contract is deployed, it would create that contract object. That would be quite useful for a couple of our modules. I'm thinking about this one, the ENS one, maybe even tests.

Maybe one of our modules already does that, but I don't recall. Also, maybe there is a good reason we didn't do that before (maybe a memory issue)? What do you think?

Prior to this commits deployment lifecycle hooks had been defined as Array<string> due
to historical reasons (contracts.js) used to be a json file back in the days.

`deployIf`, `onDeploy` and `afterDeploy` can now be defined as (async)
function and have access to several dependencies such as contract instances and web3.
However, in order to have needed dependencies registered in the dependencies object,
all lifecycle hook dependencies need to be listed in the `deps` property
as shown below.

Also note that this is NOT a breaking change. Existing deployment lifecycle
hooks written in Array<string> style still work.

All three lifecycle hooks can now be defined as (async) functions and get an dependency
object with a shape like this:

```
interface DeploymentLifecycleHookDependencies {
  contracts: Map<string, ContractInstance>;
  web3: Web3Instance
}
```

`deployIf` lifecycle hook has to return a promise (or be defined using async/await and return
a value) like this:

```
contracts: {
  MyToken: {...},
  SimpleStorage: {
    deps: ['MyToken'], // this is needed to make `MyToken` available within `dependencies`
    deployIf: async (dependencies) => {
      return dependencies.contracts.MyToken_address;
    }
  },
}
```

Vanilla promises (instead of async/await) can be used as well:

```
contracts: {
  MyToken: {...},
  SimpleStorage: {
    deps: ['MyToken'],
    deployIf: (dependencies) => {
      return new Promise(resolve => resolve(dependencies.contracts.MyToken_address);
    }
  },
}
```

`onDeploy` as well, returns either a promise or is used using async/await:

```
contracts: {
  SimpleStorage: {
    onDeploy: async (dependencies) => {
      const simpleStorage = dependencies.contracts.SimpleStorage;
      const value = await simpleStorage.methods.get().call();
      console.log(value);
    }
  },
}
```

`afterDeploy` has automatically access to all configured and deployed contracts of the dapp:

```
contracts: {
  SimpleStorage: {...},
  MyToken: {...},
  afterDeploy: (dependencies) => {
    console.log('Done!');
  }
}
```

Closes #1029
@0x-r4bbit 0x-r4bbit force-pushed the feat/deploy-hook-functions branch from 7452df5 to 77cc23b Compare November 20, 2018 08:48
@0x-r4bbit 0x-r4bbit merged commit 8b68bec into master Nov 20, 2018
@0x-r4bbit 0x-r4bbit deleted the feat/deploy-hook-functions branch November 20, 2018 15:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Support js functions for onDeploy & afterDeploy actions

4 participants